package gat.api.config.aspect;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import com.bitsun.core.common.constant.HeaderConstants;
import com.bitsun.core.common.web.ResultDTO;
import gat.api.config.annotation.NoRepeatSubmit;
import gat.common.util.AddressUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 对于某些请求禁止重复提交（幂等性），如：新购-升级-确认订单等重要操作
 * <p>
 * key = token+请求方法+请求参数（加密后的字符串）
 *
 * @author LX
 * @date 2022-04-10 16:38
 */
@Aspect
@Order(1)
@Component
@Slf4j
public class RepeatSubmitAspect {

    private static final String REPEAT_SUBMIT_TOKEN_PREFIX = "repeat_submit_token:";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Pointcut("@annotation(noRepeatSubmit)")
    public void pointcut(NoRepeatSubmit noRepeatSubmit) {

    }

    @Around(value = "pointcut(noRepeatSubmit)", argNames = "pjp,noRepeatSubmit")
    public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        long expireTime = noRepeatSubmit.expireTime();
        boolean delKey = noRepeatSubmit.delKey();
        String message = noRepeatSubmit.message();
        String token = request.getHeader(HeaderConstants.token);
        if (StringUtils.isEmpty(token)) {
            token = AddressUtil.getIpAddress(request);
        }

        String path = request.getServletPath();
        //获取请求参数,无参则不传
        String argStr = "";
        if (ObjectUtil.isNotNull(pjp.getArgs())) {
            Object arg = pjp.getArgs()[0];
            argStr = JSONUtil.toJsonStr(arg);
        } else {
            argStr = token.concat("_").concat(path);
        }
        String key = getKey(token, path, argStr);
        Boolean isSuccess = stringRedisTemplate.opsForValue().setIfAbsent(key, argStr, expireTime, TimeUnit.SECONDS);
        if (isSuccess) {
            // 获取锁成功
            Object result;
            try {
                // 执行进程
                result = pjp.proceed();
            } finally {
                // 判断是否需要解锁
                if (delKey) {
                    releaseLock(key, argStr);
                }
            }
            return result;
        } else {
            return ResultDTO.error(message);
        }
    }


    /**
     * 验证 Token 正确性
     *
     * @param key   key 字符串
     * @param value value 存储在Redis中的辅助验证信息
     */
    public void releaseLock(String key, String value) {
        // 设置 Lua 脚本，其中 KEYS[1] 是 key，KEYS[2] 是 value
        String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        // 根据 Key 前缀拼接 Key
        // 执行 Lua 脚本
        Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, value));
        // 根据返回结果判断是否成功成功匹配并删除 Redis 键值对，若果结果不为空和0，则验证通过
        if (result != null && result != 0L) {
            log.info("验证 key={},value={} 成功", key, value);
            return;
        }
        log.info("验证 key={},value={} 失败", key, value);
    }

    private String getKey(String token, String path, String argStr) {
        String key = REPEAT_SUBMIT_TOKEN_PREFIX.concat(token).concat(":").concat(path);
        if (StrUtil.isNotBlank(argStr)) {
            String sign = SecureUtil.md5(argStr);
            key = key.concat("_").concat(sign);
        }
        return key;
    }
}
